using System;
using System.Collections;
using System.Collections.Generic;
using TestCases.CompareAssert.Core.Comparers.Interfaces;
using TestCases.CompareAssert.Core.Interfaces;
using TestCases.Utils;

namespace TestCases.CompareAssert.Core.Comparers.Collections
{
    public class DictionaryComparer : CompatibleInterfacesComparer<IDictionary>
    {
        public const string IdentifiableName = "DictionaryComparer";

        public const string EqualsMessage = "All <{LPath}> and <{RPath}> entries are equals.";        

        public IObjectComparer EntryComparer { get; set; }

        public DictionaryComparer() : base("IDictionary`2")
        {            
        }

        public DictionaryComparer(IObjectComparer entryComparer) : this()
        {
            EntryComparer = entryComparer;
        }
        
        public IEqualityComparer<object> KeysComparer { get; set; }
        public Predicate<object> IgnoreEntry { get; set; }

        public override string Name
        {
            get { return IdentifiableName; }
        }

        private IEnumerable MergeCollections(IEnumerable coll1, IEnumerable coll2)
        {
            return (KeysComparer != null) ? coll1.Merge(coll2, KeysComparer) : coll1.Merge(coll2);
        }

        private IResultBuilder LeftNotContains(ICompareContext context, IList<IResultProvider> children, object key)
        {
            return this.IsNotEquals(context, children).
                        Message("<{LPath}> dictionary doesn't contains element with <{Key}> key.").
                        With(new {Key = key});
        }        
        
        private IResultBuilder RightNotContains(ICompareContext context, IList<IResultProvider> children, object key)
        {
            return this.IsNotEquals(context, children).
                        Message("<{RPath}> dictionary doesn't contains element with <{Key}> key.").
                        With(new {Key = key});
        }

        private IResultBuilder DifferentEntries(ICompareContext context, IList<IResultProvider> results, ICompareContext indexedContext)
        {
            return this.IsNotEquals(context, results).
                Message("<{LEntryPath}> not equals to <{REntryPath}> [{LEntryValue} != {REntryValue}].").
                With(new
                {
                    LEntryPath = indexedContext.LPath, 
                    REntryPath = indexedContext.RPath, 
                    LEntryValue = indexedContext.LValue,
                    REntryValue = indexedContext.RValue
                });
        }

        public override IResultBuilder CompareVia(Type interfaceType, ICompareContext context)
        {

            object leftObject = context.LValue, rightObject = context.RValue;
            var results = new List<IResultProvider>();

            const string keysProperty = "Keys";

            var keys = MergeCollections(
                leftObject.GetProperty<IEnumerable>(interfaceType, keysProperty),
                rightObject.GetProperty<IEnumerable>(interfaceType, keysProperty));

            foreach (var key in keys)
            {
                
                if (IgnoreEntry != null && IgnoreEntry(key))
                {
                    continue;
                }
                
                const string containsKeyMethod = "ContainsKey";
                if (!leftObject.InvokeMethod<bool>(interfaceType, containsKeyMethod, key))
                {
                    return LeftNotContains(context, results, key);
                }

                if (!rightObject.InvokeMethod<bool>(interfaceType, containsKeyMethod, key))
                {
                    return RightNotContains(context, results, key);
                }

                const string itemProperty = "Item";
                var leftValue = leftObject.GetProperty<object>(interfaceType, itemProperty, key);
                var rightValue = rightObject.GetProperty<object>(interfaceType, itemProperty, key);

                var indexedContext = context.ForIndexedObjects(leftValue, rightValue, key);
                if (EntryComparer.CompareAndAddResult(indexedContext, results).GetStatus() == CompareStatus.NotEquals)
                {                    
                    return DifferentEntries(context, results, indexedContext);
                }
            }
            return this.IsEquals(context, results).Message(EqualsMessage);
        }


        public override IResultBuilder CompareVia(IDictionary leftDictionary, IDictionary rightDictionary, ICompareContext context)
        {
            var results = new List<IResultProvider>();

            var keys = MergeCollections(leftDictionary.Keys, rightDictionary.Keys);            
            foreach (object key in keys)
            {
                if (IgnoreEntry != null && IgnoreEntry(key))
                {
                    continue;
                }                
                
                if (!leftDictionary.Contains(key))
                {
                    return LeftNotContains(context, results, key);
                }

                if (!rightDictionary.Contains(key))
                {
                    return RightNotContains(context, results, key);
                }

                var indexedContext = context.ForIndexedObjects(leftDictionary[key], rightDictionary[key], key);
                if (EntryComparer.CompareAndAddResult(indexedContext, results).GetStatus() == CompareStatus.NotEquals)
                {
                    return DifferentEntries(context, results, indexedContext);
                }
            }
            return this.IsEquals(context, results).Message(EqualsMessage);
        }


    }
}